/*!	 target_scanline.cpp
**	 Template File
**
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**	Copyright (c) 2008 Chris Moore
**	Copyright (c) 2012-2013 Carlos López
**
**	This package is free software; you can redistribute it and/or
**	modify it under the terms of the GNU General Public License as
**	published by the Free Software Foundation; either version 2 of
**	the License, or (at your option) any later version.
**
**	This package is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
**	General Public License for more details.
**
*/

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include "target_scanline.h"

#include "general.h"
#include <synfig/localization.h>

#include "canvas.h"
#include "context.h"
#include "render.h"
#include "string.h"
#include "surface.h"
#include "rendering/software/surfacesw.h"
#include "rendering/renderer.h"

#endif

using namespace std;
using namespace etl;
using namespace synfig;
using namespace rendering;

#define PIXEL_RENDERING_LIMIT 1500000

#define USE_PIXELRENDERING_LIMIT 1

Target_Scanline::Target_Scanline():
    threads_(2)
{
    curr_frame_ = 0;

    if (const char *s = getenv("SYNFIG_TARGET_DEFAULT_ENGINE")) {
        set_engine(s);
    }
}

int
Target_Scanline::next_frame(Time& time)
{
    return Target::next_frame(time);
}

bool
synfig::Target_Scanline::call_renderer(Context &context, const etl::handle<rendering::SurfaceSW> &surfacesw, int /* quality */, const RendDesc &renddesc, ProgressCallback * /* cb */)
{
    rendering::Task::Handle task;
    surfacesw->set_size(renddesc.get_w(), renddesc.get_h());
    {
        // TODO: quick hack
        // we need to pass already sorted context to renderer
        // when old renderer will finally removed
        CanvasBase sub_queue;
        Context sub_context;

        if (*context && (*context)->get_canvas()) {
            (*context)->get_canvas()->get_context_sorted(context.get_params(), sub_queue, sub_context);
        } else {
            sub_context = context;
        }

        task = sub_context.build_rendering_task();
    }

    if (task) {
        rendering::Renderer::Handle renderer = rendering::Renderer::get_renderer(get_engine());

        if (!renderer) {
            throw "Renderer '" + get_engine() + "' not found";
        }

        task->target_surface = surfacesw;
        task->target_surface->create();
        task->init_target_rect(RectInt(VectorInt::zero(), surfacesw->get_size()), renddesc.get_tl(), renddesc.get_br());

        rendering::Task::List list;
        list.push_back(task);
        renderer->run(list);
    }

    return true;
}

bool
synfig::Target_Scanline::render(ProgressCallback *cb)
{
    SuperCallback super_cb;
    int
    frames = 0,
    total_frames,
    quality = get_quality(),
    frame_start,
    frame_end;
    Time
    t = 0;

    assert(canvas);
    curr_frame_ = 0;

    if (!init()) {
        if (cb) {
            cb->error(_("Target initialization failure"));
        }

        return false;
    }

    frame_start = desc.get_frame_start();
    frame_end = desc.get_frame_end();

    ContextParams context_params(desc.get_render_excluded_contexts());

    // Calculate the number of frames
    total_frames = frame_end - frame_start + 1;

    if (total_frames <= 0) {
        total_frames = 1;
    }

    try {


        if (total_frames >= 1) {
            do {
                // Grab the time
                frames = next_frame(t);

                // If we have a callback, and it returns
                // false, go ahead and bail. (it may be a user cancel)
                if (cb && !cb->amount_complete(total_frames - frames, total_frames)) {
                    return false;
                }

                Context context;
                // pass the Render Method to the context
                context = canvas->get_context(context_params);
                context.set_render_method(SOFTWARE);

                // Set the time that we wish to render
                if (!get_avoid_time_sync() || canvas->get_time() != t) {
                    canvas->set_time(t);
                }

                canvas->set_outline_grow(desc.get_outline_grow());

                // If quality is set otherwise, then we use the accelerated renderer
                {
#if USE_PIXELRENDERING_LIMIT

                    if (desc.get_w()*desc.get_h() > PIXEL_RENDERING_LIMIT) {
                        SurfaceSW::Handle surfacesw(new SurfaceSW());
                        Surface &surface = surfacesw->get_surface();

                        int rowheight = PIXEL_RENDERING_LIMIT / desc.get_w();

                        if (!rowheight) {
                            rowheight = 1;    // TODO: render partial lines to stay within the limit?
                        }

                        int rows = desc.get_h() / rowheight;
                        int lastrowheight = desc.get_h() - rows * rowheight;

                        rows++;

                        synfig::info("Render broken up into %d block%s %d pixels tall, and a final block %d pixels tall",
                                     rows - 1, rows == 2 ? "" : "s", rowheight, lastrowheight);

                        // loop through all the full rows
                        if (!start_frame()) {
                            throw (string("add_frame(): target panic on start_frame()"));
                            return false;
                        }

                        for (int i = 0; i < rows; ++i) {
                            RendDesc	blockrd = desc;

                            // render the strip at the normal size unless it's the last one...
                            if (i == rows - 1) {
                                if (!lastrowheight) {
                                    break;
                                }

                                blockrd.set_subwindow(0, i * rowheight, desc.get_w(), lastrowheight);
                            } else {
                                blockrd.set_subwindow(0, i * rowheight, desc.get_w(), rowheight);
                            }

                            if (!call_renderer(context, surfacesw, quality, blockrd, 0)) {
                                if (cb) {
                                    cb->error(_("Accelerated Renderer Failure"));
                                }

                                return false;
                            } else {
                                int y;
                                int rowspan = sizeof(Color) * surface.get_w();
                                Surface::pen pen = surface.begin();

                                int yoff = i * rowheight;

                                for (y = 0; y < blockrd.get_h(); y++, pen.inc_y()) {
                                    Color *colordata = start_scanline(y + yoff);

                                    if (!colordata) {
                                        throw (string("add_frame(): call to start_scanline(y) returned NULL"));
                                        return false;
                                    }

                                    switch (get_alpha_mode()) {
                                    case TARGET_ALPHA_MODE_FILL:
                                        for (int i = 0; i < surface.get_w(); i++) {
                                            colordata[i] = Color::blend(surface[y][i], desc.get_bg_color(), 1.0f);
                                        }

                                        break;

                                    case TARGET_ALPHA_MODE_EXTRACT:
                                        for (int i = 0; i < surface.get_w(); i++) {
                                            float a = surface[y][i].get_a();
                                            colordata[i] = Color(a, a, a, a);
                                        }

                                        break;

                                    case TARGET_ALPHA_MODE_REDUCE:
                                        for (int i = 0; i < surface.get_w(); i++) {
                                            colordata[i] = Color(surface[y][i].get_r(), surface[y][i].get_g(), surface[y][i].get_b(), 1.0f);
                                        }

                                        break;

                                    case TARGET_ALPHA_MODE_KEEP:
                                        memcpy(colordata, surface[y], rowspan);
                                        break;
                                    }

                                    if (!end_scanline()) {
                                        throw (string("add_frame(): target panic on end_scanline()"));
                                        return false;
                                    }
                                }

                                surfacesw->destroy();
                            }
                        }

                        end_frame();

                    } else { // use normal rendering...
#endif
                        SurfaceSW::Handle surfacesw(new SurfaceSW());
                        Surface &surface = surfacesw->get_surface();

                        if (!call_renderer(context, surfacesw, quality, desc, 0)) {
                            // For some reason, the accelerated renderer failed.
                            if (cb) {
                                cb->error(_("Accelerated Renderer Failure"));
                            }

                            return false;
                        }

                        // Put the surface we renderer
                        // onto the target.
                        if (!add_frame(&surface)) {
                            if (cb) {
                                cb->error(_("Unable to put surface on target"));
                            }

                            return false;
                        }

#if USE_PIXELRENDERING_LIMIT
                    }

#endif
                }
            } while (frames);
        } else {
            // Set the time that we wish to render
            if (!get_avoid_time_sync() || canvas->get_time() != t) {
                canvas->set_time(t);
            }

            canvas->set_outline_grow(desc.get_outline_grow());
            Context context = canvas->get_context(context_params);

            // If quality is set otherwise, then we use the accelerated renderer
            {
#if USE_PIXELRENDERING_LIMIT

                if (desc.get_w()*desc.get_h() > PIXEL_RENDERING_LIMIT) {
                    SurfaceSW::Handle surfacesw(new SurfaceSW());
                    Surface &surface = surfacesw->get_surface();

                    int totalheight = desc.get_h();
                    int rowheight = PIXEL_RENDERING_LIMIT / desc.get_w();

                    if (!rowheight) {
                        rowheight = 1;    // TODO: render partial lines to stay within the limit?
                    }

                    int rows = desc.get_h() / rowheight;
                    int lastrowheight = desc.get_h() - rows * rowheight;

                    rows++;

                    synfig::info("Render broken up into %d block%s %d pixels tall, and a final block %d pixels tall",
                                 rows - 1, rows == 2 ? "" : "s", rowheight, lastrowheight);

                    // loop through all the full rows
                    if (!start_frame()) {
                        throw (string("add_frame(): target panic on start_frame()"));
                        return false;
                    }

                    for (int i = 0; i < rows; ++i) {
                        RendDesc	blockrd = desc;

                        // render the strip at the normal size unless it's the last one...
                        if (i == rows - 1) {
                            if (!lastrowheight) {
                                break;
                            }

                            blockrd.set_subwindow(0, i * rowheight, desc.get_w(), lastrowheight);
                        } else {
                            blockrd.set_subwindow(0, i * rowheight, desc.get_w(), rowheight);
                        }

                        SuperCallback	sc(cb, i * rowheight, (i + 1)*rowheight, totalheight);

                        if (!call_renderer(context, surfacesw, quality, blockrd, &sc)) {
                            if (cb) {
                                cb->error(_("Accelerated Renderer Failure"));
                            }

                            return false;
                        }

                        int y;
                        int rowspan = sizeof(Color) * surface.get_w();
                        Surface::pen pen = surface.begin();

                        int yoff = i * rowheight;

                        for (y = 0; y < blockrd.get_h(); y++, pen.inc_y()) {
                            Color *colordata = start_scanline(y + yoff);

                            if (!colordata) {
                                throw (string("add_frame(): call to start_scanline(y) returned NULL"));
                                return false;
                            }

                            switch (get_alpha_mode()) {
                            case TARGET_ALPHA_MODE_FILL:
                                for (int i = 0; i < surface.get_w(); i++) {
                                    colordata[i] = Color::blend(surface[y][i], desc.get_bg_color(), 1.0f);
                                }

                                break;

                            case TARGET_ALPHA_MODE_EXTRACT:
                                for (int i = 0; i < surface.get_w(); i++) {
                                    float a = surface[y][i].get_a();
                                    colordata[i] = Color(a, a, a, a);
                                }

                                break;

                            case TARGET_ALPHA_MODE_REDUCE:
                                for (int i = 0; i < surface.get_w(); i++) {
                                    colordata[i] = Color(surface[y][i].get_r(), surface[y][i].get_g(), surface[y][i].get_b(), 1.0f);
                                }

                                break;

                            case TARGET_ALPHA_MODE_KEEP:
                                memcpy(colordata, surface[y], rowspan);
                                break;
                            }

                            if (!end_scanline()) {
                                throw (string("add_frame(): target panic on end_scanline()"));
                                return false;
                            }
                        }

                        // I'm done with this part
                        sc.amount_complete(100, 100);
                    }

                    end_frame();

                } else {
#endif
                    SurfaceSW::Handle surfacesw(new SurfaceSW());
                    Surface &surface = surfacesw->get_surface();

                    if (!call_renderer(context, surfacesw, quality, desc, cb)) {
                        if (cb) {
                            cb->error(_("Accelerated Renderer Failure"));
                        }

                        return false;
                    }

                    // Put the surface we renderer
                    // onto the target.
                    if (!add_frame(&surface)) {
                        if (cb) {
                            cb->error(_("Unable to put surface on target"));
                        }

                        return false;
                    }

#if USE_PIXELRENDERING_LIMIT
                }

#endif
            }
        }

    } catch (String str) {
        if (cb) {
            cb->error(_("Caught string :") + str);
        }

        return false;
    } catch (std::bad_alloc) {
        if (cb) {
            cb->error(_("Ran out of memory (Probably a bug)"));
        }

        return false;
    } catch (...) {
        if (cb) {
            cb->error(_("Caught unknown error, rethrowing..."));
        }

        throw;
    }

    return true;
}

bool
Target_Scanline::add_frame(const Surface *surface)
{
    assert(surface);

    int y;
    int rowspan = sizeof(Color) * surface->get_w();
    Surface::const_pen pen = surface->begin();

    if (!start_frame()) {
        throw (string("add_frame(): target panic on start_frame()"));
        return false;
    }

    for (y = 0; y < surface->get_h(); y++, pen.inc_y()) {
        Color *colordata = start_scanline(y);

        if (!colordata) {
            throw (string("add_frame(): call to start_scanline(y) returned NULL"));
            return false;
        }

        switch (get_alpha_mode()) {
        case TARGET_ALPHA_MODE_FILL:
            for (int i = 0; i < surface->get_w(); i++) {
                colordata[i] = Color::blend((*surface)[y][i], desc.get_bg_color(), 1.0f);
            }

            break;

        case TARGET_ALPHA_MODE_EXTRACT:
            for (int i = 0; i < surface->get_w(); i++) {
                float a = (*surface)[y][i].get_a();
                colordata[i] = Color(a, a, a, a);
            }

            break;

        case TARGET_ALPHA_MODE_REDUCE:
            for (int i = 0; i < surface->get_w(); i++) {
                colordata[i] = Color((*surface)[y][i].get_r(), (*surface)[y][i].get_g(), (*surface)[y][i].get_b(), 1.0f);
            }

            break;

        case TARGET_ALPHA_MODE_KEEP:
            memcpy(colordata, (*surface)[y], rowspan);
            break;
        }

        if (!end_scanline()) {
            throw (string("add_frame(): target panic on end_scanline()"));
            return false;
        }
    }

    end_frame();

    return true;
}